<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="RKKec" id="RKKec"><span data-lake-id="uf503b38f" id="uf503b38f">典型回答</span></h1>
  <p data-lake-id="uaf7ba969" id="uaf7ba969"><br></p>
  <p data-lake-id="u24cc3e64" id="u24cc3e64"><span data-lake-id="u8ea2e87a" id="u8ea2e87a">所谓本地缓存，就是和应用服务器在一起的缓存工具，将需要缓存的数据放到本地缓存中，可以大大的提升访问速度。</span></p>
  <p data-lake-id="u980ae000" id="u980ae000"><span data-lake-id="ua587360f" id="ua587360f">​</span><br></p>
  <p data-lake-id="ue26e997a" id="ue26e997a"><span data-lake-id="u6b56b388" id="u6b56b388">在设计本地缓存时，一般需要考虑以下几个方面的问题:</span></p>
  <p data-lake-id="uc0e998ef" id="uc0e998ef"><br></p>
  <h4 data-lake-id="rqM60" id="rqM60"><span data-lake-id="u558c42b8" id="u558c42b8" style="color: rgb(0, 0, 0)">数据结构</span></h4>
  <p data-lake-id="u5492793d" id="u5492793d"><span data-lake-id="u79b0b006" id="u79b0b006" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">一般来讲，为了提升缓存的效率，通常采用Key-Value结构进行数据存储，也就是说，缓存中的数据保存和读取都需要有一个Key，通过Key来读取固定的缓存的Value。</span></p>
  <h4 data-lake-id="RB2gI" id="RB2gI"><span data-lake-id="ub67fe7f2" id="ub67fe7f2" style="color: rgb(0, 0, 0)">线程安全</span></h4>
  <p data-lake-id="uf31a49dd" id="uf31a49dd"><span data-lake-id="ubce37c21" id="ubce37c21" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">本地缓存一定要考虑线程安全的问题，因为大多数情况下本地缓存都是一个全局可访问的变量，那么就会有多个线程同时访问，所以线程安全问题不容忽视。</span></p>
  <h4 data-lake-id="IuOKf" id="IuOKf"><span data-lake-id="udff897ac" id="udff897ac" style="color: rgb(0, 0, 0)">对象上限</span></h4>
  <p data-lake-id="ue17db70c" id="ue17db70c"><span data-lake-id="u1fba143a" id="u1fba143a" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">因为是本地缓存，而本地内存中的数据是要占用JVM的堆内存的，所以内存是有上限要求的，如果无限存储，最终一定会导致OOM的问题。</span></p>
  <h4 data-lake-id="PqftN" id="PqftN"><span data-lake-id="u71e1bf84" id="u71e1bf84" style="color: rgb(0, 0, 0)">清除策略</span></h4>
  <p data-lake-id="u1e57a303" id="u1e57a303"><span data-lake-id="uff087c57" id="uff087c57" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">为了避免OOM的问题，一般会考虑在缓存中增加清除策略，通过一定的手段定期的清理掉一些数据，来保证内存占用不会过大，常见清除策略主要有有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等;</span></p>
  <h4 data-lake-id="p7bNN" id="p7bNN"><span data-lake-id="ub350dc8e" id="ub350dc8e" style="color: rgb(0, 0, 0)">过期时间</span></h4>
  <p data-lake-id="u083eb2ec" id="u083eb2ec"><span data-lake-id="u18639a1a" id="u18639a1a" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">有了清除策略并不能保证百分百的可以删除数据，极端情况会会使得某些数据一直无法删除。这时候就需要有一种机制，能够保证某些K-V一定可以删除。通常采用的方案是给每一个缓存的key设置过期时间，当达到过期时间之后直接删除,采用清除策略+过期时间双重保证;</span></p>
  <p data-lake-id="u6dc5f1d0" id="u6dc5f1d0"><br></p>
  <p data-lake-id="u848cd4e5" id="u848cd4e5"><span data-lake-id="u3163ea4f" id="u3163ea4f" class="lake-fontsize-11">考虑到以上这些问题之后，就可以考虑如何具体实现一个本地缓存了。</span></p>
  <p data-lake-id="u586fd1d5" id="u586fd1d5"><span data-lake-id="ufe84c3a9" id="ufe84c3a9" class="lake-fontsize-11">​</span><br></p>
  <p data-lake-id="u6b2d6306" id="u6b2d6306"><strong><span data-lake-id="u8c53c6f5" id="u8c53c6f5" class="lake-fontsize-11">最简单的方式是通过HashMap来实现一个本地缓存，因为他本身就是一种Key-Value结构的，并且如果使用ConcurrentHashMap的话，也能保证线程安全，不过需要自己实现对象上限、过期策略以及清除策略。</span></strong></p>
  <p data-lake-id="uf8dfdacc" id="uf8dfdacc"><strong><span data-lake-id="ufbfc5e47" id="ufbfc5e47" class="lake-fontsize-11">​</span></strong><br></p>
  <p data-lake-id="u00ebb12e" id="u00ebb12e"><span data-lake-id="uf4cdbdd5" id="uf4cdbdd5">除此之外，也有一些比较成熟的开源的本地缓存框架可以直接使用，比较常用的有：</span></p>
  <p data-lake-id="u9b787e05" id="u9b787e05"><span data-lake-id="u96f21ee6" id="u96f21ee6">​</span><br></p>
  <ul list="ube6e83a4">
   <li fid="u98906e30" data-lake-id="u9e4df005" id="u9e4df005"><span data-lake-id="uccb5ae5a" id="uccb5ae5a">Guava Cache</span></li>
   <li fid="u98906e30" data-lake-id="uf3d03bd1" id="uf3d03bd1"><span data-lake-id="uf3c511b6" id="uf3c511b6">Caffeine </span><strong><span data-lake-id="uf9e19fcb" id="uf9e19fcb">（推荐）</span></strong></li>
   <li fid="u98906e30" data-lake-id="uc96f56fa" id="uc96f56fa"><span data-lake-id="u1911438c" id="u1911438c">Encache</span></li>
  </ul>
  <p data-lake-id="u8d4c9386" id="u8d4c9386"><br></p>
  <p data-lake-id="u8ae7a9fe" id="u8ae7a9fe"><span data-lake-id="u7a21411a" id="u7a21411a">推荐优先使用Caffeine作为本地缓存，在功能上，GuavaCache支持的功能，Caffeine都支持，另外Caffeine支持异步Cache和写入外部资源，这两个Guava Cache是不支持的。Caffeine也是Spring 5中默认支持的Cache。而Caffeine在性能上要比GuavaCache好很多，主要有以下几个原因：</span></p>
  <p data-lake-id="u0976359c" id="u0976359c"><span data-lake-id="u13e3273c" id="u13e3273c">​</span><br></p>
  <ol list="u62b1b405">
   <li fid="u1753b92b" data-lake-id="udd9a85f1" id="udd9a85f1"><strong><span data-lake-id="ud1cb107a" id="ud1cb107a">剔除算法</span></strong><span data-lake-id="u6cef86e2" id="u6cef86e2">，GuavaCache采用的是「LRU」算法，而Caffeine采用的是「Window TinyLFU」算法，这是两者之间最大，也是根本的区别。</span></li>
   <li fid="u1753b92b" data-lake-id="ud71eb36c" id="ud71eb36c"><strong><span data-lake-id="u86e5cde2" id="u86e5cde2">立即失效</span></strong><span data-lake-id="u213dd41c" id="u213dd41c">，Guava会把立即失效 (例如：expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffiene能正确识别这种剔除原因。</span></li>
   <li fid="u1753b92b" data-lake-id="ud6f3efe1" id="ud6f3efe1"><strong><span data-lake-id="u9ed1afa7" id="u9ed1afa7">取代提醒</span></strong><span data-lake-id="u2e98fc61" id="u2e98fc61">，Guava只要数据被替换，不管什么原因，都会触发剔除监听器。而Caffiene在取代值和先前值的引用完全一样时不会触发监听器。</span></li>
   <li fid="u1753b92b" data-lake-id="u1932b0d2" id="u1932b0d2"><strong><span data-lake-id="u061b5479" id="u061b5479">异步化</span></strong><span data-lake-id="u29571bc8" id="u29571bc8">，Caffiene的很多工作都是交给线程池去做的（默认：ForkJoinPool.commonPool()），例如：剔除监听器，刷新机制，维护工作等。</span></li>
  </ol>
  <p data-lake-id="u930bbb30" id="u930bbb30"><span data-lake-id="u9a4455fa" id="u9a4455fa">​</span><br></p>
  <p data-lake-id="u094c8e86" id="u094c8e86"><br></p>
  <h1 data-lake-id="FN1wh" id="FN1wh"><span data-lake-id="u8697d40f" id="u8697d40f">扩展知识</span></h1>
  <p data-lake-id="u6877be58" id="u6877be58"><br></p>
  <h2 data-lake-id="oBB7X" id="oBB7X"><span data-lake-id="ufab940a1" id="ufab940a1">基于Caffeine实现本地缓存</span></h2>
  <p data-lake-id="u8eae5290" id="u8eae5290"><br></p>
  <pre lang="java"><code>
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 本地缓存工具
 *
 * @author hollis
 */
@Component
public class LocalCacheManager implements InitializingBean {

    private Cache&lt;String, String&gt; localCache;

    /**
     * 向缓存中保存数据，如果已经存在则不覆盖
     */
    public void putIfNotExist(String key, String value) {
        if (localCache.getIfPresent(key) == null) {
            localCache.put(key, value);
        }
    }

    /**
     * 根据key获取缓存数据
     *
     * @param key
     */
    public String get(String key) {
        return localCache.getIfPresent(key);
    }

    public void del(String key) {
        localCache.invalidate(key);
    }

    /**
     * 在bean初始化时，初始化本地缓存
     */
    @Override
    public void afterPropertiesSet() {
        localCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .expireAfterAccess(10, TimeUnit.SECONDS)
            .maximumSize(1000)
            .build();
    }
}
</code></pre>
 </body>
</html>